<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>什么是BM25算法？</title>
    <style>
        body {
            font-family: 'Segoe UI', 'Microsoft YaHei', Arial, sans-serif;
            background: #f6f8fa;
            margin: 0;
            padding: 0;
        }

        .container {
            max-width: 900px;
            margin: 40px auto;
            background: #fff;
            border-radius: 12px;
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
            padding: 32px 24px;
        }

        h1 {
            color: #e67e22;
            text-align: center;
        }

        h2 {
            color: #b45f06;
        }

        p,
        li {
            font-size: 1.1em;
            color: #333;
            line-height: 1.7;
        }

        .tip {
            background: #fdf6e3;
            border-left: 4px solid #e67e22;
            padding: 12px 18px;
            margin: 18px 0;
            border-radius: 6px;
            color: #b45f06;
        }

        .desc-box {
            background: #f9f9f9;
            border-radius: 6px;
            padding: 12px 18px;
            margin: 18px 0;
            border-left: 4px solid #e67e22;
        }

        .bm25-demo {
            display: flex;
            justify-content: center;
            margin: 32px 0;
        }

        .input-group {
            margin: 18px 0 0 0;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        input[type="text"] {
            width: 320px;
            padding: 6px 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 1em;
        }

        button {
            background: #e67e22;
            color: #fff;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            font-size: 1em;
            cursor: pointer;
        }

        button:hover {
            background: #b45f06;
        }

        .result-list {
            margin-top: 18px;
            background: #f9f6ef;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(230, 126, 34, 0.08);
            padding: 18px 16px;
        }

        .result-item {
            margin-bottom: 12px;
            padding-bottom: 8px;
            border-bottom: 1px solid #f2e6d9;
        }

        .result-item:last-child {
            border-bottom: none;
        }

        .score {
            color: #e67e22;
            font-weight: bold;
            margin-left: 8px;
        }

        pre code {
            font-size: 1.15em;
            line-height: 1.7;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/github.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
</head>

<body>
    <div class="container">
        <h1>什么是BM25算法？</h1>
        <h2>1. 基础定义</h2>
        <p>BM25 是一种常用的<strong>文本相关性排序算法</strong>，广泛应用于搜索引擎和信息检索系统。它能根据关键词和文档的匹配程度，为每个文档打分并排序。</p>
        <div class="tip">
            <strong>通俗理解：</strong> BM25 就像"关键词找文章"的打分裁判，分数越高，内容越相关。</p>
        </div>
        <h2>2. 生活类比</h2>
        <ul>
            <li>你在图书馆找"人工智能"，BM25 会优先推荐标题、内容中多次出现"人工智能"的书。</li>
            <li>短小精悍的文章更容易得高分，太长的文章分数会被"稀释"。</li>
        </ul>
        <h2>3. BM25的核心思想与公式</h2>
        <div class="desc-box">
            <strong>核心思想：</strong> 关键词在文档中出现得越多，文档越短，且关键词越少见，分数越高。
            <br><br>
            <strong>公式：</strong><br>
            <span style="font-size:1.1em;">
                \[
                \text{score}(D, Q) = \sum_{q \in Q} \frac{IDF(q) \cdot f(q, D) \cdot (k_1 + 1)}{f(q, D) + k_1 \cdot (1 -
                b + b
                \cdot \frac{|D|}{\text{avgdl}})}
                \]
            </span>
            <br>
            其中：<br>
            - \(f(q, D)\)：关键词q在文档D中出现的次数<br>
            - \(|D|\)：文档D的长度<br>
            - \(\text{avgdl}\)：所有文档的平均长度<br>
            - \(k_1, b\)：调节参数，常用 \(k_1=1.5, b=0.75\)<br>
            - \(IDF(q)\)：关键词q的逆文档频率，越少见分值越高<br>
            - ∑表示对查询Q 中的每一个词q，都计算一遍后面的分数，然后把所有词的分数加起来，得到最终的相关性分数<br>
        </div>
        <div class="desc-box">
            <h4>1. 公式整体结构</h4>
            <ul>
                <li><b>score(D, Q)</b>：表示文档 D 和查询 Q（关键词集合）之间的相关性得分。分数越高，说明文档和查询越相关。</li>
                <li>这个公式对查询中的每个关键词 <span style="font-family:monospace">q</span> 都计算一次得分，然后把所有关键词的得分加起来。</li>
            </ul>
            <h4>2. 公式各部分含义</h4>
            <ul>
                <li><b>f(q, D)</b>：关键词 <span style="font-family:monospace">q</span> 在文档 <span
                        style="font-family:monospace">D</span> 中出现的次数。出现得越多，分数越高。</li>
                <li><b>|D|</b>：文档 <span style="font-family:monospace">D</span> 的长度（通常是词数）。文档越长，分数会被"稀释"。</li>
                <li><b>avgdl</b>：所有文档的平均长度。用来做长度归一化，防止长文档天然得分高。</li>
                <li><b>k1, b</b>：调节参数，常用 <span style="font-family:monospace">k1 = 1.5, b =
                        0.75</span>。k1控制词频影响，b控制长度归一化强度。</li>
                <li><b>IDF(q)</b>：关键词 <span style="font-family:monospace">q</span> 的逆文档频率。关键词越少见，IDF越高，分数提升越多。</li>
            </ul>
            <h4>3. 公式的核心思想</h4>
            <ul>
                <li><b>关键词在文档中出现得越多，分数越高。</b></li>
                <li><b>文档越短，分数越高。</b>（因为短文档更"专注"）</li>
                <li><b>关键词越少见，分数越高。</b>（稀有词更有区分度）</li>
                <li><b>参数 k1, b 用于平衡词频和文档长度的影响。</b></li>
            </ul>
            <h4>4. 生活类比</h4>
            <ul>
                <li>你在图书馆找"人工智能"相关的书：</li>
                <ul>
                    <li>书A标题和内容多次出现"人工智能"，得分高。</li>
                    <li>书B内容很长但只提到一次"人工智能"，得分低。</li>
                    <li>"人工智能"这个词在所有书里都出现，区分度低，得分也低。</li>
                </ul>
            </ul>
            <h4>5. 公式分子和分母的作用</h4>
            <ul>
                <li><b>分子：</b><span style="font-family:monospace">IDF(q) * f(q, D) * (k1 + 1)</span> ——
                    关键词越稀有、出现次数越多，分数越高。</li>
                <li><b>分母：</b><span style="font-family:monospace">f(q, D) + k1 * (1 - b + b * |D|/avgdl)</span> ——
                    控制词频的"收益递减"，并对文档长度做归一化，防止长文档"刷分"。</li>
            </ul>
            <h4>6. 总结</h4>
            <p>BM25 公式的本质就是：<br><b>"关键词出现得多、文档短、关键词稀有" → 分数高，排序靠前。</b></p>
        </div>
        <h2>4. 主要应用场景</h2>
        <ul>
            <li>搜索引擎：根据用户输入的关键词排序网页</li>
            <li>问答系统：找出最相关的答案</li>
            <li>与向量检索结合，实现"语义+关键词"混合检索</li>
        </ul>
        <h2>5. BM25相关性排序</h2>
        <p>输入关键词，系统会用BM25算法为下方文档打分并排序：</p>
        <p>文档：</p>
        <ul>
            <li>人工智能正在改变世界。</li>
            <li>机器学习和深度学习是人工智能的重要分支。</li>
            <li>猫和狗是常见的宠物。</li>
            <li>AI可以帮助医生诊断疾病。</li>
            <li>篮球是一项受欢迎的运动。</li>
        </ul>
        <div class="bm25-demo">
            <div class="input-group">
                <input type="text" id="query-input" placeholder="输入关键词，如：人工智能">
                <button id="search-btn">相关性排序</button>
            </div>
        </div>
        <div class="result-list" id="result-list"></div>
        <div class="tip">
            <strong>小结：</strong> BM25 是文本检索领域的"经典算法"，与向量检索结合后，能让AI搜索既懂"关键词"又懂"语义"。
        </div>
        <script>
            // 定义一个包含若干中文文档的数组
            const docs = [
                '人工 智能 正在 改变世界。',
                '机器学习 和 深度学习 是 人工智能的重要分支。',
                '猫和狗 是常见的宠物。',
                'AI可以 帮助医生诊断疾病。',
                '篮球 是一项受欢迎的运动。',
                '人工智能 与大数据密不可分。',
                '天气预报 依赖于大量数据分析。'
            ];

            // 分词函数：将文本按中文、字母、数字和空格分割成词语
            function tokenize(text) {
                // 先去除除中文、字母、数字、空格以外的字符，再按空格分割，最后过滤掉空字符串
                return text.replace(/[^\u4e00-\u9fa5a-zA-Z0-9 ]/g, '').split(/\s+/).filter(Boolean);
            }

            // 计算IDF（逆文档频率）：衡量关键词在所有文档中的稀有程度
            function computeIDF(term, docs) {
                // 统计包含该关键词的文档数
                let df = 0;
                for (const doc of docs) {
                    // 如果该文档分词后包含该关键词，则df加1
                    if (tokenize(doc).includes(term)) df++;
                }
                // 按照BM25的IDF公式计算并返回
                return Math.log((docs.length - df + 0.5) / (df + 0.5) + 1);
            }

            // BM25打分函数：计算某个文档与查询关键词的相关性得分
            function bm25Score(query, doc, docs, k1 = 1.5, b = 0.75) {
                // 对查询进行分词
                const terms = tokenize(query);
                // 对文档进行分词
                const docTerms = tokenize(doc);
                // 当前文档的长度
                const docLen = docTerms.length;
                // 所有文档的平均长度
                const avgdl = docs.reduce((sum, d) => sum + tokenize(d).length, 0) / docs.length;
                // 初始化得分
                let score = 0;
                // 遍历每个查询词
                for (const q of terms) {
                    // 统计该查询词在当前文档中出现的次数
                    const fq = docTerms.filter(t => t === q).length;
                    // 如果该词在文档中未出现，跳过
                    if (fq === 0) continue;
                    // 计算该词的IDF
                    const idf = computeIDF(q, docs);
                    // 按照BM25公式累加得分
                    score += idf * fq * (k1 + 1) / (fq + k1 * (1 - b + b * docLen / avgdl));
                }
                // 返回最终得分
                return score;
            }

            // 展示排序结果的函数
            function showResults() {debugger
                // 获取用户输入的查询关键词，并去除首尾空格
                const query = document.getElementById('query-input').value.trim();
                // 获取用于显示结果的DOM元素
                const resultList = document.getElementById('result-list');
                // 如果没有输入关键词，提示用户输入
                if (!query) {
                    resultList.innerHTML = '请输入关键词';
                    return;
                }
                // 对每个文档计算BM25得分，并过滤掉得分为0的文档
                const scored = docs.map((doc, idx) => ({
                    doc,
                    score: bm25Score(query, doc, docs)
                }))
                // 按得分从高到低排序
                scored.sort((a, b) => b.score - a.score);
                // 如果没有相关内容，提示用户
                if (scored.length === 0) {
                    resultList.innerHTML = '没有找到相关内容';
                    return;
                }
                // 将排序后的结果渲染到页面
                resultList.innerHTML = scored.map(item => `
    
      ${item.doc}
      分数：${item.score.toFixed(3)}
    
  `).join('<br/>');
            }

            // 给"相关性排序"按钮绑定点击事件，点击时执行showResults函数
            document.getElementById('search-btn').onclick = showResults;

        </script>

    </div>
</body>

</html>